use stencil_tysys::{IntEncoding, IntType, UserTypeId, ValueType};

/// Concrete type with a compile-time knowable size that a field can take.
#[derive(Clone, Debug)]
pub enum FieldType {
    /// Zero sized, not actually accessible.
    Void,

    /// Bool.
    Bool,

    /// Some integer type.
    Int(IntEncoding),

    /// Flat chunk of bytes of some size.
    Buf(usize),

    /// Another struct available in the context of the module.  Cannot form
    /// cycles, must reference another struct.
    Struct(UserTypeId),

    /// Fixed size array of a particular size.
    Array(usize, Box<FieldType>),
}

impl FieldType {
    /// Constructs an int array type of some size.
    pub fn int_array(size: usize, ienc: IntEncoding) -> Self {
        Self::Int(ienc).in_array(size)
    }

    /// Converts a type into an array type of some size.
    pub fn in_array(self, size: usize) -> Self {
        Self::Array(size, Box::new(self))
    }

    /// Returns if this is a "pure" primitive void type.
    pub fn is_primitive_void(&self) -> bool {
        matches!(self, Self::Void)
    }

    /// Returns more generally if this is a void type, even if it's wrapped by
    /// an array, even though that doesn't make sense.
    pub fn is_general_void(&self) -> bool {
        match self {
            Self::Void => true,
            Self::Array(_, inner) => inner.is_general_void(),
            _ => false,
        }
    }

    /// Returns if the type is a primitive type.  Ie, if we can trivially know
    /// how to read it at any arbitrary offset.
    pub fn is_primitive(&self) -> bool {
        match self {
            Self::Void | Self::Bool | Self::Int(_) | Self::Buf(_) | Self::Struct(_) => true,
            Self::Array(_, _) => false,
        }
    }

    /// Computes the dimensionality of a field type.  For most types this is 0, but
    /// layers of arrays add dimensions.  This is how we decide how many index
    /// variables accessors need to have.
    pub fn get_dimensions(&self) -> usize {
        match self {
            Self::Array(_, aty) => 1 + aty.get_dimensions(),
            _ => 0,
        }
    }

    /// Gets the count of the number of primitives contained in the field.
    pub fn get_prim_count(&self) -> usize {
        match self {
            FieldType::Void => 0,
            FieldType::Bool => 1,
            FieldType::Int(_) => 1,
            FieldType::Buf(_) => 1,
            FieldType::Struct(_) => 1,
            FieldType::Array(len, v) => len * v.get_prim_count(),
        }
    }
}

impl From<IntEncoding> for FieldType {
    fn from(value: IntEncoding) -> Self {
        Self::Int(value)
    }
}

impl Into<ValueType> for &FieldType {
    fn into(self) -> ValueType {
        match self {
            FieldType::Void => ValueType::Void,
            FieldType::Bool => ValueType::Bool,
            FieldType::Int(ienc) => ienc.ty().into(),
            FieldType::Buf(size) => ValueType::ByteArray(*size),
            FieldType::Struct(id) => ValueType::ObjectInst(*id),
            // FIXME make this translate with size?
            FieldType::Array(_len, aty) => ValueType::List(Box::new(aty.as_ref().into())),
        }
    }
}

/// Parses primitive types from a string to a `FieldType`.  This will only
/// correctly parse `void`, `bool`, primitive ints, and bufs.
pub fn parse_basic_type_str(s: &str) -> Option<FieldType> {
    use IntType::*;

    // Try to parse bufs.
    if let Some(term) = s.strip_prefix("buf") {
        match term.parse::<usize>() {
            Ok(len) => return Some(FieldType::Buf(len)),
            _ => return None,
        }
    }

    // Try to parse the primitives.
    let ft = match s {
        "void" => FieldType::Void,
        "bool" => FieldType::Bool,
        "u8" => U8.to_big_encoding().into(),
        "i8" => I8.to_big_encoding().into(),
        "u16" => U16.to_big_encoding().into(),
        "i16" => I16.to_big_encoding().into(),
        "u32" => U32.to_big_encoding().into(),
        "i32" => I32.to_big_encoding().into(),
        "u64" => U64.to_big_encoding().into(),
        "i64" => I64.to_big_encoding().into(),
        _ => return None,
    };

    Some(ft)
}
